Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

[red-knot] Move UnionBuilder tests to Markdown #15374

Merged
merged 2 commits into from
Jan 9, 2025
Merged

Conversation

sharkdp
Copy link
Contributor

@sharkdp sharkdp commented Jan 9, 2025

Summary

This moves almost all of our existing UnionBuilder tests to a Markdown-based test suite.

I see how this could be a more controversial change, since these tests where written specifically for UnionBuilder, and by creating the union types using Python type expressions, we add an additional layer on top (parsing and inference of these expressions) that moves these tests away from clean unit tests more in the direction of integration tests. Also, there are probably a few implementation details of UnionBuilder hidden in the test assertions (e.g. order of union elements after simplifications).

That said, I think we would like to see all those properties that are being tested here from any implementation of union types. And the Markdown tests come with the usual advantages:

  • More consice
  • Better readability
  • No re-compiliation when working on tests
  • Easier to add additional explanations and structure to the test suite

This changeset adds a few additional tests, but keeps the logic of the existing tests except for a few minor modifications for consistency.

This moves almost all of our existing `UnionBuilder` tests to a
Markdown-based test suite.

I see how this could be a more controversial change, since these tests
where written specifically for `UnionBuilder`, and by creating the union
types using Python type expressions, we add an additional layer on top
(parsing and inference of these expressions) that move these tests
away from clean unit tests more in the direction of integration tests.
Also, there are probably a few implementation details of `UnionBuilder`
hidden in the test assertions (e.g. order of union elements after
simplifications).

That said, I think we would like to see all those properties that are
being tested here from *any* implementation of union types. And the
Markdown tests come with the usual advantages:

- Much more consice
- Better readability
- No re-compiliation when working on tests
- Easier to add additional explanations and structure to the test suite
@sharkdp sharkdp added the red-knot Multi-file analysis & type inference label Jan 9, 2025
Copy link
Contributor

github-actions bot commented Jan 9, 2025

ruff-ecosystem results

Linter (stable)

✅ ecosystem check detected no linter changes.

Linter (preview)

✅ ecosystem check detected no linter changes.

Comment on lines +55 to +61
def _(
u1: str | LiteralString, u2: LiteralString | str, u3: Literal["a"] | str | LiteralString, u4: str | bytes | LiteralString
) -> None:
reveal_type(u1) # revealed: str
reveal_type(u2) # revealed: str
reveal_type(u3) # revealed: str
reveal_type(u4) # revealed: str | bytes
Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

These tests would be more readable if we had something like C++'s std::declval<T>() (#13789) which pretends to create a value of type T. Using knot_extensions, we could now easily implement a function like the following (using PEP 747 language):

def value_of[T](typ: TypeForm[T]) -> T: ...

that would allow us to write these tests in a single line without having to introduce dummy parameters and without having the definitions a few lines away from the assertions:

Suggested change
def _(
u1: str | LiteralString, u2: LiteralString | str, u3: Literal["a"] | str | LiteralString, u4: str | bytes | LiteralString
) -> None:
reveal_type(u1) # revealed: str
reveal_type(u2) # revealed: str
reveal_type(u3) # revealed: str
reveal_type(u4) # revealed: str | bytes
reveal_type(value_of(str | LiteralString)) # revealed: str
reveal_type(value_of(LiteralString | str)) # revealed: str
reveal_type(value_of(Literal["a"] | str | LiteralString)) # revealed: str
reveal_type(value_of(str | bytes | LiteralString)) # revealed: str | bytes

A disadvantage of this approach would be that it makes more of these tests dependent on knot_extensions. @AlexWaygood is still "a little sceptical" about this idea, so I kept the usual strategy of using function parameters.

Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Interestingly, typing.cast (with a dummy second argument), is effectively the value_of function. I wouldn't be opposed to adding value_of as a variant that doesn't require the dummy second argument, at the same time we add typing.cast. But I don't feel strongly either way, I think the function arguments are reasonable too.

Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

A disadvantage of this approach would be that it makes more of these tests dependent on knot_extensions.

Also, could consider reveal_type_of_value(...) function which has same logic of reveal_type that doesn't need to explicitly import?

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Also, could consider reveal_type_of_value(...) function which has same logic of reveal_type that doesn't need to explicitly import?

More like reveal_type_of_type_expression or something like that, but yes — that's a good idea! We could take it one step further and implement the analog to assert_type on that level. Basically assert_equal_type(type1, type2).

And now that I write it out, I realize that we sort-of already have this in knot_extensions. We can write

static_assert(is_equivalent_to(str | LiteralString, str))

Now that only works for fully static types, but once we have is_gradual_equivalent_to, that would be a (slightly more verbose) alternative to assert_equal_type:

static_assert(is_gradual_equivalent_to(Unknown | Unknown | str, Unknown | str))

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Interestingly, typing.cast (with a dummy second argument), is effectively the value_of function.

"It's like typing.cast, just without having a value in the first place!" is exactly how I pitched it to Alex, and he still didn't want it 😄

Copy link
Member

@AlexWaygood AlexWaygood left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Nice. I think this is definitely worthwhile

@AlexWaygood AlexWaygood added the testing Related to testing Ruff itself label Jan 9, 2025
Copy link
Contributor

@carljm carljm left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I haven't reviewed in detail, happy to let Alex do that for efficiency's sake. Just clarifying that I'm on board with moving these tests to mdtest, I think the pros outweigh the cons. I think almost any test where we have to use the Ty enum and construct arbitrary types, is probably better as an mdtest.

Comment on lines +55 to +61
def _(
u1: str | LiteralString, u2: LiteralString | str, u3: Literal["a"] | str | LiteralString, u4: str | bytes | LiteralString
) -> None:
reveal_type(u1) # revealed: str
reveal_type(u2) # revealed: str
reveal_type(u3) # revealed: str
reveal_type(u4) # revealed: str | bytes
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Interestingly, typing.cast (with a dummy second argument), is effectively the value_of function. I wouldn't be opposed to adding value_of as a variant that doesn't require the dummy second argument, at the same time we add typing.cast. But I don't feel strongly either way, I think the function arguments are reasonable too.

Co-authored-by: Alex Waygood <[email protected]>
Co-authored-by: T-256 <[email protected]>
@sharkdp sharkdp force-pushed the david/union-type-tests branch from 42a2c73 to 4eec725 Compare January 9, 2025 20:16
@sharkdp sharkdp merged commit b33cf5b into main Jan 9, 2025
21 checks passed
@sharkdp sharkdp deleted the david/union-type-tests branch January 9, 2025 20:45
dcreager added a commit that referenced this pull request Jan 9, 2025
* main:
  [red-knot] Move `UnionBuilder` tests to Markdown (#15374)
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
red-knot Multi-file analysis & type inference testing Related to testing Ruff itself
Projects
None yet
Development

Successfully merging this pull request may close these issues.

4 participants